// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package qunar.tc.decompiler.main;

import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import qunar.tc.decompiler.code.CodeConstants;
import qunar.tc.decompiler.jdk827.List827;
import qunar.tc.decompiler.main.ClassesProcessor.ClassNode;
import qunar.tc.decompiler.main.collectors.BytecodeMappingTracer;
import qunar.tc.decompiler.main.extern.IFernflowerLogger;
import qunar.tc.decompiler.main.extern.IFernflowerPreferences;
import qunar.tc.decompiler.main.rels.ClassWrapper;
import qunar.tc.decompiler.main.rels.MethodWrapper;
import qunar.tc.decompiler.modules.decompiler.ExprProcessor;
import qunar.tc.decompiler.modules.decompiler.exps.*;
import qunar.tc.decompiler.modules.decompiler.stats.RootStatement;
import qunar.tc.decompiler.modules.decompiler.vars.VarTypeProcessor;
import qunar.tc.decompiler.modules.decompiler.vars.VarVersionPair;
import qunar.tc.decompiler.modules.renamer.PoolInterceptor;
import qunar.tc.decompiler.struct.StructClass;
import qunar.tc.decompiler.struct.StructField;
import qunar.tc.decompiler.struct.StructMember;
import qunar.tc.decompiler.struct.StructMethod;
import qunar.tc.decompiler.struct.attr.*;
import qunar.tc.decompiler.struct.consts.PrimitiveConstant;
import qunar.tc.decompiler.struct.gen.FieldDescriptor;
import qunar.tc.decompiler.struct.gen.MethodDescriptor;
import qunar.tc.decompiler.struct.gen.VarType;
import qunar.tc.decompiler.struct.gen.generics.*;
import qunar.tc.decompiler.util.InterpreterUtil;
import qunar.tc.decompiler.util.TextBuffer;

import java.util.*;

public class ClassWriter {
    private final PoolInterceptor interceptor;

    public ClassWriter() {
        interceptor = DecompilerContext.getPoolInterceptor();
    }

    private static void invokeProcessors(ClassNode node) {
        ClassWrapper wrapper = node.getWrapper();
        StructClass cl = wrapper.getClassStruct();

        InitializerProcessor.extractInitializers(wrapper);

        if (node.type == ClassNode.CLASS_ROOT &&
                !cl.isVersionGE_1_5() &&
                DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_CLASS_1_4)) {
            ClassReference14Processor.processClassReferences(node);
        }

        if (cl.hasModifier(CodeConstants.ACC_ENUM) && DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_ENUM)) {
            EnumProcessor.clearEnum(wrapper);
        }

        if (DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_ASSERTIONS)) {
            AssertProcessor.buildAssertions(node);
        }
    }

    public void classLambdaToJava(ClassNode node, TextBuffer buffer, Exprent method_object, int indent, BytecodeMappingTracer origTracer) {
        ClassWrapper wrapper = node.getWrapper();
        if (wrapper == null) {
            return;
        }

        boolean lambdaToAnonymous = DecompilerContext.getOption(IFernflowerPreferences.LAMBDA_TO_ANONYMOUS_CLASS);

        ClassNode outerNode = (ClassNode) DecompilerContext.getProperty(DecompilerContext.CURRENT_CLASS_NODE);
        DecompilerContext.setProperty(DecompilerContext.CURRENT_CLASS_NODE, node);

        BytecodeMappingTracer tracer = new BytecodeMappingTracer(origTracer.getCurrentSourceLine());

        try {
            StructClass cl = wrapper.getClassStruct();

            DecompilerContext.getLogger().startWriteClass(node.simpleName);

            if (node.lambdaInformation.is_method_reference) {
                if (!node.lambdaInformation.is_content_method_static && method_object != null) {
                    // reference to a virtual method
                    buffer.append(method_object.toJava(indent, tracer));
                } else {
                    // reference to a static method
                    buffer.append(ExprProcessor.getCastTypeName(new VarType(node.lambdaInformation.content_class_name, true)));
                }

                buffer.append("::")
                        .append(CodeConstants.INIT_NAME.equals(node.lambdaInformation.content_method_name) ? "new" : node.lambdaInformation.content_method_name);
            } else {
                // lambda method
                StructMethod mt = cl.getMethod(node.lambdaInformation.content_method_key);
                MethodWrapper methodWrapper = wrapper.getMethodWrapper(mt.getName(), mt.getDescriptor());
                MethodDescriptor md_content = MethodDescriptor.parseDescriptor(node.lambdaInformation.content_method_descriptor);
                MethodDescriptor md_lambda = MethodDescriptor.parseDescriptor(node.lambdaInformation.method_descriptor);

                if (!lambdaToAnonymous) {
                    buffer.append('(');

                    boolean firstParameter = true;
                    int index = node.lambdaInformation.is_content_method_static ? 0 : 1;
                    int start_index = md_content.params.length - md_lambda.params.length;

                    for (int i = 0; i < md_content.params.length; i++) {
                        if (i >= start_index) {
                            if (!firstParameter) {
                                buffer.append(", ");
                            }

                            String parameterName = methodWrapper.varproc.getVarName(new VarVersionPair(index, 0));
                            buffer.append(parameterName == null ? "param" + index : parameterName); // null iff decompiled with errors

                            firstParameter = false;
                        }

                        index += md_content.params[i].stackSize;
                    }

                    buffer.append(") ->");
                }

                buffer.append(" {").appendLineSeparator();
                tracer.incrementCurrentSourceLine();

                methodLambdaToJava(node, wrapper, mt, buffer, indent + 1, !lambdaToAnonymous, tracer);

                buffer.appendIndent(indent).append("}");

                addTracer(cl, mt, tracer);
            }
        } finally {
            DecompilerContext.setProperty(DecompilerContext.CURRENT_CLASS_NODE, outerNode);
        }

        DecompilerContext.getLogger().endWriteClass();
    }

    public void classToJava(ClassNode node, TextBuffer buffer, int indent, BytecodeMappingTracer tracer) {
        ClassNode outerNode = (ClassNode) DecompilerContext.getProperty(DecompilerContext.CURRENT_CLASS_NODE);
        DecompilerContext.setProperty(DecompilerContext.CURRENT_CLASS_NODE, node);

        int startLine = tracer != null ? tracer.getCurrentSourceLine() : 0;
        BytecodeMappingTracer dummy_tracer = new BytecodeMappingTracer(startLine);

        try {
            // last minute processing
            invokeProcessors(node);

            ClassWrapper wrapper = node.getWrapper();
            StructClass cl = wrapper.getClassStruct();

            DecompilerContext.getLogger().startWriteClass(cl.qualifiedName);

            // write class definition
            int start_class_def = buffer.length();
            writeClassDefinition(node, buffer, indent);

            boolean hasContent = false;
            boolean enumFields = false;

            dummy_tracer.incrementCurrentSourceLine(buffer.countLines(start_class_def));

            for (StructField fd : cl.getFields()) {
                boolean hide = fd.isSynthetic() && DecompilerContext.getOption(IFernflowerPreferences.REMOVE_SYNTHETIC) ||
                        wrapper.getHiddenMembers().contains(InterpreterUtil.makeUniqueKey(fd.getName(), fd.getDescriptor()));
                if (hide) continue;

                boolean isEnum = fd.hasModifier(CodeConstants.ACC_ENUM) && DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_ENUM);
                if (isEnum) {
                    if (enumFields) {
                        buffer.append(',').appendLineSeparator();
                        dummy_tracer.incrementCurrentSourceLine();
                    }
                    enumFields = true;
                } else if (enumFields) {
                    buffer.append(';');
                    buffer.appendLineSeparator();
                    buffer.appendLineSeparator();
                    dummy_tracer.incrementCurrentSourceLine(2);
                    enumFields = false;
                }

                fieldToJava(wrapper, cl, fd, buffer, indent + 1, dummy_tracer); // FIXME: insert real tracer

                hasContent = true;
            }

            if (enumFields) {
                buffer.append(';').appendLineSeparator();
                dummy_tracer.incrementCurrentSourceLine();
            }

            // FIXME: fields don't matter at the moment
            startLine += buffer.countLines(start_class_def);

            // methods
            for (StructMethod mt : cl.getMethods()) {
                boolean hide = mt.isSynthetic() && DecompilerContext.getOption(IFernflowerPreferences.REMOVE_SYNTHETIC) ||
                        mt.hasModifier(CodeConstants.ACC_BRIDGE) && DecompilerContext.getOption(IFernflowerPreferences.REMOVE_BRIDGE) ||
                        wrapper.getHiddenMembers().contains(InterpreterUtil.makeUniqueKey(mt.getName(), mt.getDescriptor()));
                if (hide) continue;

                int position = buffer.length();
                int storedLine = startLine;
                if (hasContent) {
                    buffer.appendLineSeparator();
                    startLine++;
                }
                BytecodeMappingTracer method_tracer = new BytecodeMappingTracer(startLine);
                boolean methodSkipped = !methodToJava(node, mt, buffer, indent + 1, method_tracer);
                if (!methodSkipped) {
                    hasContent = true;
                    addTracer(cl, mt, method_tracer);
                    startLine = method_tracer.getCurrentSourceLine();
                } else {
                    buffer.setLength(position);
                    startLine = storedLine;
                }
            }

            // member classes
            for (ClassNode inner : node.nested) {
                if (inner.type == ClassNode.CLASS_MEMBER) {
                    StructClass innerCl = inner.classStruct;
                    boolean isSynthetic = (inner.access & CodeConstants.ACC_SYNTHETIC) != 0 || innerCl.isSynthetic();
                    boolean hide = isSynthetic && DecompilerContext.getOption(IFernflowerPreferences.REMOVE_SYNTHETIC) ||
                            wrapper.getHiddenMembers().contains(innerCl.qualifiedName);
                    if (hide) continue;

                    if (hasContent) {
                        buffer.appendLineSeparator();
                        startLine++;
                    }
                    BytecodeMappingTracer class_tracer = new BytecodeMappingTracer(startLine);
                    classToJava(inner, buffer, indent + 1, class_tracer);
                    startLine = buffer.countLines();

                    hasContent = true;
                }
            }

            buffer.appendIndent(indent).append('}');

            if (node.type != ClassNode.CLASS_ANONYMOUS) {
                buffer.appendLineSeparator();
            }
        } finally {
            DecompilerContext.setProperty(DecompilerContext.CURRENT_CLASS_NODE, outerNode);
        }

        DecompilerContext.getLogger().endWriteClass();
    }

    private static void addTracer(StructClass cls, StructMethod method, BytecodeMappingTracer tracer) {
        StructLineNumberTableAttribute table = method.getAttribute(StructGeneralAttribute.ATTRIBUTE_LINE_NUMBER_TABLE);
        tracer.setLineNumberTable(table);
        String key = InterpreterUtil.makeUniqueKey(method.getName(), method.getDescriptor());
        DecompilerContext.getBytecodeSourceMapper().addTracer(cls.qualifiedName, key, tracer);
    }

    private void writeClassDefinition(ClassNode node, TextBuffer buffer, int indent) {
        if (node.type == ClassNode.CLASS_ANONYMOUS) {
            buffer.append(" {").appendLineSeparator();
            return;
        }

        ClassWrapper wrapper = node.getWrapper();
        StructClass cl = wrapper.getClassStruct();

        int flags = node.type == ClassNode.CLASS_ROOT ? cl.getAccessFlags() : node.access;
        boolean isDeprecated = cl.hasAttribute(StructGeneralAttribute.ATTRIBUTE_DEPRECATED);
        boolean isSynthetic = (flags & CodeConstants.ACC_SYNTHETIC) != 0 || cl.hasAttribute(StructGeneralAttribute.ATTRIBUTE_SYNTHETIC);
        boolean isEnum = DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_ENUM) && (flags & CodeConstants.ACC_ENUM) != 0;
        boolean isInterface = (flags & CodeConstants.ACC_INTERFACE) != 0;
        boolean isAnnotation = (flags & CodeConstants.ACC_ANNOTATION) != 0;

        if (isDeprecated) {
            appendDeprecation(buffer, indent);
        }

        if (interceptor != null) {
            String oldName = interceptor.getOldName(cl.qualifiedName);
            appendRenameComment(buffer, oldName, MType.CLASS, indent);
        }

        if (isSynthetic) {
            appendComment(buffer, "synthetic class", indent);
        }

        appendAnnotations(buffer, indent, cl, -1);

        buffer.appendIndent(indent);

        if (isEnum) {
            // remove abstract and final flags (JLS 8.9 Enums)
            flags &= ~CodeConstants.ACC_ABSTRACT;
            flags &= ~CodeConstants.ACC_FINAL;
        }

        appendModifiers(buffer, flags, CLASS_ALLOWED, isInterface, CLASS_EXCLUDED);

        if (isEnum) {
            buffer.append("enum ");
        } else if (isInterface) {
            if (isAnnotation) {
                buffer.append('@');
            }
            buffer.append("interface ");
        } else {
            buffer.append("class ");
        }

        buffer.append(node.simpleName);

        GenericClassDescriptor descriptor = getGenericClassDescriptor(cl);
        if (descriptor != null && !descriptor.fparameters.isEmpty()) {
            appendTypeParameters(buffer, descriptor.fparameters, descriptor.fbounds);
        }

        buffer.append(' ');

        if (!isEnum && !isInterface && cl.superClass != null) {
            VarType supertype = new VarType(cl.superClass.getString(), true);
            if (!VarType.VARTYPE_OBJECT.equals(supertype)) {
                buffer.append("extends ");
                if (descriptor != null) {
                    buffer.append(GenericMain.getGenericCastTypeName(descriptor.superclass));
                } else {
                    buffer.append(ExprProcessor.getCastTypeName(supertype));
                }
                buffer.append(' ');
            }
        }

        if (!isAnnotation) {
            int[] interfaces = cl.getInterfaces();
            if (interfaces.length > 0) {
                buffer.append(isInterface ? "extends " : "implements ");
                for (int i = 0; i < interfaces.length; i++) {
                    if (i > 0) {
                        buffer.append(", ");
                    }
                    if (descriptor != null) {
                        buffer.append(GenericMain.getGenericCastTypeName(descriptor.superinterfaces.get(i)));
                    } else {
                        buffer.append(ExprProcessor.getCastTypeName(new VarType(cl.getInterface(i), true)));
                    }
                }
                buffer.append(' ');
            }
        }

        buffer.append('{').appendLineSeparator();
    }

    private void fieldToJava(ClassWrapper wrapper, StructClass cl, StructField fd, TextBuffer buffer, int indent, BytecodeMappingTracer tracer) {
        int start = buffer.length();
        boolean isInterface = cl.hasModifier(CodeConstants.ACC_INTERFACE);
        boolean isDeprecated = fd.hasAttribute(StructGeneralAttribute.ATTRIBUTE_DEPRECATED);
        boolean isEnum = fd.hasModifier(CodeConstants.ACC_ENUM) && DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_ENUM);

        if (isDeprecated) {
            appendDeprecation(buffer, indent);
        }

        if (interceptor != null) {
            String oldName = interceptor.getOldName(cl.qualifiedName + " " + fd.getName() + " " + fd.getDescriptor());
            appendRenameComment(buffer, oldName, MType.FIELD, indent);
        }

        if (fd.isSynthetic()) {
            appendComment(buffer, "synthetic field", indent);
        }

        appendAnnotations(buffer, indent, fd, TypeAnnotation.FIELD);

        buffer.appendIndent(indent);

        if (!isEnum) {
            appendModifiers(buffer, fd.getAccessFlags(), FIELD_ALLOWED, isInterface, FIELD_EXCLUDED);
        }

        VarType fieldType = new VarType(fd.getDescriptor(), false);

        GenericFieldDescriptor descriptor = null;
        if (DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES)) {
            StructGenericSignatureAttribute attr = fd.getAttribute(StructGeneralAttribute.ATTRIBUTE_SIGNATURE);
            if (attr != null) {
                descriptor = GenericMain.parseFieldSignature(attr.getSignature());
            }
        }

        if (!isEnum) {
            if (descriptor != null) {
                buffer.append(GenericMain.getGenericCastTypeName(descriptor.type));
            } else {
                buffer.append(ExprProcessor.getCastTypeName(fieldType));
            }
            buffer.append(' ');
        }

        buffer.append(fd.getName());

        tracer.incrementCurrentSourceLine(buffer.countLines(start));

        Exprent initializer;
        if (fd.hasModifier(CodeConstants.ACC_STATIC)) {
            initializer = wrapper.getStaticFieldInitializers().getWithKey(InterpreterUtil.makeUniqueKey(fd.getName(), fd.getDescriptor()));
        } else {
            initializer = wrapper.getDynamicFieldInitializers().getWithKey(InterpreterUtil.makeUniqueKey(fd.getName(), fd.getDescriptor()));
        }
        if (initializer != null) {
            if (isEnum && initializer.type == Exprent.EXPRENT_NEW) {
                NewExprent expr = (NewExprent) initializer;
                expr.setEnumConst(true);
                buffer.append(expr.toJava(indent, tracer));
            } else {
                buffer.append(" = ");

                if (initializer.type == Exprent.EXPRENT_CONST) {
                    ((ConstExprent) initializer).adjustConstType(fieldType);
                }

                // FIXME: special case field initializer. Can map to more than one method (constructor) and bytecode instruction
                buffer.append(initializer.toJava(indent, tracer));
            }
        } else if (fd.hasModifier(CodeConstants.ACC_FINAL) && fd.hasModifier(CodeConstants.ACC_STATIC)) {
            StructConstantValueAttribute attr = fd.getAttribute(StructGeneralAttribute.ATTRIBUTE_CONSTANT_VALUE);
            if (attr != null) {
                PrimitiveConstant constant = cl.getPool().getPrimitiveConstant(attr.getIndex());
                buffer.append(" = ");
                buffer.append(new ConstExprent(fieldType, constant.value, null).toJava(indent, tracer));
            }
        }

        if (!isEnum) {
            buffer.append(";").appendLineSeparator();
            tracer.incrementCurrentSourceLine();
        }
    }

    private static void methodLambdaToJava(ClassNode lambdaNode,
                                           ClassWrapper classWrapper,
                                           StructMethod mt,
                                           TextBuffer buffer,
                                           int indent,
                                           boolean codeOnly, BytecodeMappingTracer tracer) {
        MethodWrapper methodWrapper = classWrapper.getMethodWrapper(mt.getName(), mt.getDescriptor());

        MethodWrapper outerWrapper = (MethodWrapper) DecompilerContext.getProperty(DecompilerContext.CURRENT_METHOD_WRAPPER);
        DecompilerContext.setProperty(DecompilerContext.CURRENT_METHOD_WRAPPER, methodWrapper);

        try {
            String method_name = lambdaNode.lambdaInformation.method_name;
            MethodDescriptor md_content = MethodDescriptor.parseDescriptor(lambdaNode.lambdaInformation.content_method_descriptor);
            MethodDescriptor md_lambda = MethodDescriptor.parseDescriptor(lambdaNode.lambdaInformation.method_descriptor);

            if (!codeOnly) {
                buffer.appendIndent(indent);
                buffer.append("public ");
                buffer.append(method_name);
                buffer.append("(");

                boolean firstParameter = true;
                int index = lambdaNode.lambdaInformation.is_content_method_static ? 0 : 1;
                int start_index = md_content.params.length - md_lambda.params.length;

                for (int i = 0; i < md_content.params.length; i++) {
                    if (i >= start_index) {
                        if (!firstParameter) {
                            buffer.append(", ");
                        }

                        String typeName = ExprProcessor.getCastTypeName(md_content.params[i].copy());
                        if (ExprProcessor.UNDEFINED_TYPE_STRING.equals(typeName) &&
                                DecompilerContext.getOption(IFernflowerPreferences.UNDEFINED_PARAM_TYPE_OBJECT)) {
                            typeName = ExprProcessor.getCastTypeName(VarType.VARTYPE_OBJECT);
                        }

                        buffer.append(typeName);
                        buffer.append(" ");

                        String parameterName = methodWrapper.varproc.getVarName(new VarVersionPair(index, 0));
                        buffer.append(parameterName == null ? "param" + index : parameterName); // null iff decompiled with errors

                        firstParameter = false;
                    }

                    index += md_content.params[i].stackSize;
                }

                buffer.append(") {").appendLineSeparator();

                indent += 1;
            }

            RootStatement root = classWrapper.getMethodWrapper(mt.getName(), mt.getDescriptor()).root;
            if (!methodWrapper.decompiledWithErrors) {
                if (root != null) { // check for existence
                    try {
                        buffer.append(root.toJava(indent, tracer));
                    } catch (Throwable t) {
                        String message = "Method " + mt.getName() + " " + mt.getDescriptor() + " couldn't be written.";
                        DecompilerContext.getLogger().writeMessage(message, IFernflowerLogger.Severity.WARN, t);
                        methodWrapper.decompiledWithErrors = true;
                    }
                }
            }

            if (methodWrapper.decompiledWithErrors) {
                buffer.appendIndent(indent);
                buffer.append("// $FF: Couldn't be decompiled");
                buffer.appendLineSeparator();
            }

            if (root != null) {
                tracer.addMapping(root.getDummyExit().bytecode);
            }

            if (!codeOnly) {
                indent -= 1;
                buffer.appendIndent(indent).append('}').appendLineSeparator();
            }
        } finally {
            DecompilerContext.setProperty(DecompilerContext.CURRENT_METHOD_WRAPPER, outerWrapper);
        }
    }

    private static String toValidJavaIdentifier(String name) {
        if (name == null || name.isEmpty()) return name;

        boolean changed = false;
        StringBuilder res = new StringBuilder(name.length());
        for (int i = 0; i < name.length(); i++) {
            char c = name.charAt(i);
            if ((i == 0 && !Character.isJavaIdentifierStart(c))
                    || (i > 0 && !Character.isJavaIdentifierPart(c))) {
                changed = true;
                res.append("_");
            } else {
                res.append(c);
            }
        }
        if (!changed) {
            return name;
        }
        return res.append("/* $FF was: ").append(name).append("*/").toString();
    }

    private boolean methodToJava(ClassNode node, StructMethod mt, TextBuffer buffer, int indent, BytecodeMappingTracer tracer) {
        ClassWrapper wrapper = node.getWrapper();
        StructClass cl = wrapper.getClassStruct();
        MethodWrapper methodWrapper = wrapper.getMethodWrapper(mt.getName(), mt.getDescriptor());

        boolean hideMethod = false;
        int start_index_method = buffer.length();

        MethodWrapper outerWrapper = (MethodWrapper) DecompilerContext.getProperty(DecompilerContext.CURRENT_METHOD_WRAPPER);
        DecompilerContext.setProperty(DecompilerContext.CURRENT_METHOD_WRAPPER, methodWrapper);

        try {
            boolean isInterface = cl.hasModifier(CodeConstants.ACC_INTERFACE);
            boolean isAnnotation = cl.hasModifier(CodeConstants.ACC_ANNOTATION);
            boolean isEnum = cl.hasModifier(CodeConstants.ACC_ENUM) && DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_ENUM);
            boolean isDeprecated = mt.hasAttribute(StructGeneralAttribute.ATTRIBUTE_DEPRECATED);
            boolean clinit = false, init = false, dinit = false;

            MethodDescriptor md = MethodDescriptor.parseDescriptor(mt.getDescriptor());

            int flags = mt.getAccessFlags();
            if ((flags & CodeConstants.ACC_NATIVE) != 0) {
                flags &= ~CodeConstants.ACC_STRICT; // compiler bug: a strictfp class sets all methods to strictfp
            }
            if (CodeConstants.CLINIT_NAME.equals(mt.getName())) {
                flags &= CodeConstants.ACC_STATIC; // ignore all modifiers except 'static' in a static initializer
            }

            if (isDeprecated) {
                appendDeprecation(buffer, indent);
            }

            if (interceptor != null) {
                String oldName = interceptor.getOldName(cl.qualifiedName + " " + mt.getName() + " " + mt.getDescriptor());
                appendRenameComment(buffer, oldName, MType.METHOD, indent);
            }

            boolean isSynthetic = (flags & CodeConstants.ACC_SYNTHETIC) != 0 || mt.hasAttribute(StructGeneralAttribute.ATTRIBUTE_SYNTHETIC);
            boolean isBridge = (flags & CodeConstants.ACC_BRIDGE) != 0;
            if (isSynthetic) {
                appendComment(buffer, "synthetic method", indent);
            }
            if (isBridge) {
                appendComment(buffer, "bridge method", indent);
            }

            appendAnnotations(buffer, indent, mt, TypeAnnotation.METHOD_RETURN_TYPE);

            buffer.appendIndent(indent);

            appendModifiers(buffer, flags, METHOD_ALLOWED, isInterface, METHOD_EXCLUDED);

            if (isInterface && !mt.hasModifier(CodeConstants.ACC_STATIC) && mt.containsCode()) {
                // 'default' modifier (Java 8)
                buffer.append("default ");
            }

            String name = mt.getName();
            if (CodeConstants.INIT_NAME.equals(name)) {
                if (node.type == ClassNode.CLASS_ANONYMOUS) {
                    name = "";
                    dinit = true;
                } else {
                    name = node.simpleName;
                    init = true;
                }
            } else if (CodeConstants.CLINIT_NAME.equals(name)) {
                name = "";
                clinit = true;
            }

            GenericMethodDescriptor descriptor = null;
            if (DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES)) {
                StructGenericSignatureAttribute attr = mt.getAttribute(StructGeneralAttribute.ATTRIBUTE_SIGNATURE);
                if (attr != null) {
                    descriptor = GenericMain.parseMethodSignature(attr.getSignature());
                    if (descriptor != null) {
                        long actualParams = md.params.length;
                        List<VarVersionPair> mask = methodWrapper.synthParameters;
                        if (mask != null) {
                            //actualParams = mask.stream().filter(Objects::isNull).count();
                            ArrayList<VarVersionPair> list = Lists.newArrayList(mask);
                            actualParams = List827.filter(list, new Predicate<VarVersionPair>() {
                                @Override
                                public boolean apply(VarVersionPair input) {
                                    return input == null;
                                }
                            }).size();
                        } else if (isEnum && init) {
                            actualParams -= 2;
                        }
                        if (actualParams != descriptor.parameterTypes.size()) {
                            String message = "Inconsistent generic signature in method " + mt.getName() + " " + mt.getDescriptor() + " in " + cl.qualifiedName;
                            DecompilerContext.getLogger().writeMessage(message, IFernflowerLogger.Severity.WARN);
                            descriptor = null;
                        }
                    }
                }
            }

            boolean throwsExceptions = false;
            int paramCount = 0;

            if (!clinit && !dinit) {
                boolean thisVar = !mt.hasModifier(CodeConstants.ACC_STATIC);

                if (descriptor != null && !descriptor.typeParameters.isEmpty()) {
                    appendTypeParameters(buffer, descriptor.typeParameters, descriptor.typeParameterBounds);
                    buffer.append(' ');
                }

                if (!init) {
                    if (descriptor != null) {
                        buffer.append(GenericMain.getGenericCastTypeName(descriptor.returnType));
                    } else {
                        buffer.append(ExprProcessor.getCastTypeName(md.ret));
                    }
                    buffer.append(' ');
                }

                buffer.append(toValidJavaIdentifier(name));
                buffer.append('(');

                List<VarVersionPair> mask = methodWrapper.synthParameters;

                int lastVisibleParameterIndex = -1;
                for (int i = 0; i < md.params.length; i++) {
                    if (mask == null || mask.get(i) == null) {
                        lastVisibleParameterIndex = i;
                    }
                }

                List<StructMethodParametersAttribute.Entry> methodParameters = null;
                if (DecompilerContext.getOption(IFernflowerPreferences.USE_METHOD_PARAMETERS)) {
                    StructMethodParametersAttribute attr = mt.getAttribute(StructGeneralAttribute.ATTRIBUTE_METHOD_PARAMETERS);
                    if (attr != null) {
                        methodParameters = attr.getEntries();
                    }
                }

                int index = isEnum && init ? 3 : thisVar ? 1 : 0;
                int start = isEnum && init ? 2 : 0;
                for (int i = start; i < md.params.length; i++) {
                    if (mask == null || mask.get(i) == null) {
                        if (paramCount > 0) {
                            buffer.append(", ");
                        }

                        appendParameterAnnotations(buffer, mt, paramCount);

                        if (methodParameters != null && i < methodParameters.size()) {
                            appendModifiers(buffer, methodParameters.get(i).myAccessFlags, CodeConstants.ACC_FINAL, isInterface, 0);
                        } else if (methodWrapper.varproc.getVarFinal(new VarVersionPair(index, 0)) == VarTypeProcessor.VAR_EXPLICIT_FINAL) {
                            buffer.append("final ");
                        }

                        String typeName;
                        boolean isVarArg = i == lastVisibleParameterIndex && mt.hasModifier(CodeConstants.ACC_VARARGS);

                        if (descriptor != null) {
                            GenericType parameterType = descriptor.parameterTypes.get(paramCount);
                            isVarArg &= parameterType.arrayDim > 0;
                            if (isVarArg) {
                                parameterType = parameterType.decreaseArrayDim();
                            }
                            typeName = GenericMain.getGenericCastTypeName(parameterType);
                        } else {
                            VarType parameterType = md.params[i];
                            isVarArg &= parameterType.arrayDim > 0;
                            if (isVarArg) {
                                parameterType = parameterType.decreaseArrayDim();
                            }
                            typeName = ExprProcessor.getCastTypeName(parameterType);
                        }

                        if (ExprProcessor.UNDEFINED_TYPE_STRING.equals(typeName) &&
                                DecompilerContext.getOption(IFernflowerPreferences.UNDEFINED_PARAM_TYPE_OBJECT)) {
                            typeName = ExprProcessor.getCastTypeName(VarType.VARTYPE_OBJECT);
                        }
                        buffer.append(typeName);
                        if (isVarArg) {
                            buffer.append("...");
                        }

                        buffer.append(' ');

                        String parameterName;
                        if (methodParameters != null && i < methodParameters.size()) {
                            parameterName = methodParameters.get(i).myName;
                        } else {
                            parameterName = methodWrapper.varproc.getVarName(new VarVersionPair(index, 0));
                        }
                        buffer.append(parameterName == null ? "param" + index : parameterName); // null iff decompiled with errors

                        paramCount++;
                    }

                    index += md.params[i].stackSize;
                }

                buffer.append(')');

                StructExceptionsAttribute attr = mt.getAttribute(StructGeneralAttribute.ATTRIBUTE_EXCEPTIONS);
                if ((descriptor != null && !descriptor.exceptionTypes.isEmpty()) || attr != null) {
                    throwsExceptions = true;
                    buffer.append(" throws ");

                    for (int i = 0; i < attr.getThrowsExceptions().size(); i++) {
                        if (i > 0) {
                            buffer.append(", ");
                        }
                        if (descriptor != null && !descriptor.exceptionTypes.isEmpty()) {
                            GenericType type = descriptor.exceptionTypes.get(i);
                            buffer.append(GenericMain.getGenericCastTypeName(type));
                        } else {
                            VarType type = new VarType(attr.getExcClassname(i, cl.getPool()), true);
                            buffer.append(ExprProcessor.getCastTypeName(type));
                        }
                    }
                }
            }

            tracer.incrementCurrentSourceLine(buffer.countLines(start_index_method));

            if ((flags & (CodeConstants.ACC_ABSTRACT | CodeConstants.ACC_NATIVE)) != 0) { // native or abstract method (explicit or interface)
                if (isAnnotation) {
                    StructAnnDefaultAttribute attr = mt.getAttribute(StructGeneralAttribute.ATTRIBUTE_ANNOTATION_DEFAULT);
                    if (attr != null) {
                        buffer.append(" default ");
                        buffer.append(attr.getDefaultValue().toJava(0, BytecodeMappingTracer.DUMMY));
                    }
                }

                buffer.append(';');
                buffer.appendLineSeparator();
            } else {
                if (!clinit && !dinit) {
                    buffer.append(' ');
                }

                // We do not have line information for method start, lets have it here for now
                buffer.append('{').appendLineSeparator();
                tracer.incrementCurrentSourceLine();

                RootStatement root = wrapper.getMethodWrapper(mt.getName(), mt.getDescriptor()).root;

                if (root != null && !methodWrapper.decompiledWithErrors) { // check for existence
                    try {
                        // to restore in case of an exception
                        BytecodeMappingTracer codeTracer = new BytecodeMappingTracer(tracer.getCurrentSourceLine());
                        TextBuffer code = root.toJava(indent + 1, codeTracer);

                        hideMethod = (code.length() == 0) && (clinit || dinit || hideConstructor(node, init, throwsExceptions, paramCount, flags));

                        buffer.append(code);

                        tracer.setCurrentSourceLine(codeTracer.getCurrentSourceLine());
                        tracer.addTracer(codeTracer);
                    } catch (Throwable t) {
                        String message = "Method " + mt.getName() + " " + mt.getDescriptor() + " couldn't be written.";
                        DecompilerContext.getLogger().writeMessage(message, IFernflowerLogger.Severity.WARN, t);
                        methodWrapper.decompiledWithErrors = true;
                    }
                }

                if (methodWrapper.decompiledWithErrors) {
                    buffer.appendIndent(indent + 1);
                    buffer.append("// $FF: Couldn't be decompiled");
                    buffer.appendLineSeparator();
                    tracer.incrementCurrentSourceLine();
                } else if (root != null) {
                    tracer.addMapping(root.getDummyExit().bytecode);
                }
                buffer.appendIndent(indent).append('}').appendLineSeparator();
            }

            tracer.incrementCurrentSourceLine();
        } finally {
            DecompilerContext.setProperty(DecompilerContext.CURRENT_METHOD_WRAPPER, outerWrapper);
        }

        // save total lines
        // TODO: optimize
        //tracer.setCurrentSourceLine(buffer.countLines(start_index_method));

        return !hideMethod;
    }

    private static boolean hideConstructor(ClassNode node, boolean init, boolean throwsExceptions, int paramCount, int methodAccessFlags) {

        if (!init || throwsExceptions || paramCount > 0 || !DecompilerContext.getOption(IFernflowerPreferences.HIDE_DEFAULT_CONSTRUCTOR)) {
            return false;
        }

        ClassWrapper wrapper = node.getWrapper();
        StructClass cl = wrapper.getClassStruct();

        int classAccesFlags = node.type == ClassNode.CLASS_ROOT ? cl.getAccessFlags() : node.access;
        boolean isEnum = cl.hasModifier(CodeConstants.ACC_ENUM) && DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_ENUM);

        // default constructor requires same accessibility flags. Exception: enum constructor which is always private
        if (!isEnum && ((classAccesFlags & ACCESSIBILITY_FLAGS) != (methodAccessFlags & ACCESSIBILITY_FLAGS))) {
            return false;
        }

        int count = 0;
        for (StructMethod mt : cl.getMethods()) {
            if (CodeConstants.INIT_NAME.equals(mt.getName())) {
                if (++count > 1) {
                    return false;
                }
            }
        }

        return true;
    }

    private static void appendDeprecation(TextBuffer buffer, int indent) {
        buffer.appendIndent(indent).append("/** @deprecated */").appendLineSeparator();
    }

    private enum MType {CLASS, FIELD, METHOD}

    private static void appendRenameComment(TextBuffer buffer, String oldName, MType type, int indent) {
        if (oldName == null) return;

        buffer.appendIndent(indent);
        buffer.append("// $FF: renamed from: ");

        switch (type) {
            case CLASS:
                buffer.append(ExprProcessor.buildJavaClassName(oldName));
                break;

            case FIELD:
                String[] fParts = oldName.split(" ");
                FieldDescriptor fd = FieldDescriptor.parseDescriptor(fParts[2]);
                buffer.append(fParts[1]);
                buffer.append(' ');
                buffer.append(getTypePrintOut(fd.type));
                break;

            default:
                String[] mParts = oldName.split(" ");
                MethodDescriptor md = MethodDescriptor.parseDescriptor(mParts[2]);
                buffer.append(mParts[1]);
                buffer.append(" (");
                boolean first = true;
                for (VarType paramType : md.params) {
                    if (!first) {
                        buffer.append(", ");
                    }
                    first = false;
                    buffer.append(getTypePrintOut(paramType));
                }
                buffer.append(") ");
                buffer.append(getTypePrintOut(md.ret));
        }

        buffer.appendLineSeparator();
    }

    private static String getTypePrintOut(VarType type) {
        String typeText = ExprProcessor.getCastTypeName(type, false);
        if (ExprProcessor.UNDEFINED_TYPE_STRING.equals(typeText) &&
                DecompilerContext.getOption(IFernflowerPreferences.UNDEFINED_PARAM_TYPE_OBJECT)) {
            typeText = ExprProcessor.getCastTypeName(VarType.VARTYPE_OBJECT, false);
        }
        return typeText;
    }

    private static void appendComment(TextBuffer buffer, String comment, int indent) {
        buffer.appendIndent(indent).append("// $FF: ").append(comment).appendLineSeparator();
    }

    private static final StructGeneralAttribute.Key[] ANNOTATION_ATTRIBUTES = {
            StructGeneralAttribute.ATTRIBUTE_RUNTIME_VISIBLE_ANNOTATIONS, StructGeneralAttribute.ATTRIBUTE_RUNTIME_INVISIBLE_ANNOTATIONS};
    private static final StructGeneralAttribute.Key[] PARAMETER_ANNOTATION_ATTRIBUTES = {
            StructGeneralAttribute.ATTRIBUTE_RUNTIME_VISIBLE_PARAMETER_ANNOTATIONS, StructGeneralAttribute.ATTRIBUTE_RUNTIME_INVISIBLE_PARAMETER_ANNOTATIONS};
    private static final StructGeneralAttribute.Key[] TYPE_ANNOTATION_ATTRIBUTES = {
            StructGeneralAttribute.ATTRIBUTE_RUNTIME_VISIBLE_TYPE_ANNOTATIONS, StructGeneralAttribute.ATTRIBUTE_RUNTIME_INVISIBLE_TYPE_ANNOTATIONS};

    private static void appendAnnotations(TextBuffer buffer, int indent, StructMember mb, int targetType) {
        Set<String> filter = new HashSet<>();

        for (StructGeneralAttribute.Key<?> key : ANNOTATION_ATTRIBUTES) {
            StructAnnotationAttribute attribute = (StructAnnotationAttribute) mb.getAttribute(key);
            if (attribute != null) {
                for (AnnotationExprent annotation : attribute.getAnnotations()) {
                    String text = annotation.toJava(indent, BytecodeMappingTracer.DUMMY).toString();
                    filter.add(text);
                    buffer.append(text).appendLineSeparator();
                }
            }
        }

        appendTypeAnnotations(buffer, indent, mb, targetType, -1, filter);
    }

    private static void appendParameterAnnotations(TextBuffer buffer, StructMethod mt, int param) {
        Set<String> filter = new HashSet<>();

        for (StructGeneralAttribute.Key<?> key : PARAMETER_ANNOTATION_ATTRIBUTES) {
            StructAnnotationParameterAttribute attribute = (StructAnnotationParameterAttribute) mt.getAttribute(key);
            if (attribute != null) {
                List<List<AnnotationExprent>> annotations = attribute.getParamAnnotations();
                if (param < annotations.size()) {
                    for (AnnotationExprent annotation : annotations.get(param)) {
                        String text = annotation.toJava(-1, BytecodeMappingTracer.DUMMY).toString();
                        filter.add(text);
                        buffer.append(text).append(' ');
                    }
                }
            }
        }

        appendTypeAnnotations(buffer, -1, mt, TypeAnnotation.METHOD_PARAMETER, param, filter);
    }

    private static void appendTypeAnnotations(TextBuffer buffer, int indent, StructMember mb, int targetType, int index, Set<String> filter) {
        for (StructGeneralAttribute.Key<?> key : TYPE_ANNOTATION_ATTRIBUTES) {
            StructTypeAnnotationAttribute attribute = (StructTypeAnnotationAttribute) mb.getAttribute(key);
            if (attribute != null) {
                for (TypeAnnotation annotation : attribute.getAnnotations()) {
                    if (annotation.isTopLevel() && annotation.getTargetType() == targetType && (index < 0 || annotation.getIndex() == index)) {
                        String text = annotation.getAnnotation().toJava(indent, BytecodeMappingTracer.DUMMY).toString();
                        if (!filter.contains(text)) {
                            buffer.append(text);
                            if (indent < 0) {
                                buffer.append(' ');
                            } else {
                                buffer.appendLineSeparator();
                            }
                        }
                    }
                }
            }
        }
    }

    private static final Map<Integer, String> MODIFIERS;

    static {
        MODIFIERS = new LinkedHashMap<>();
        MODIFIERS.put(CodeConstants.ACC_PUBLIC, "public");
        MODIFIERS.put(CodeConstants.ACC_PROTECTED, "protected");
        MODIFIERS.put(CodeConstants.ACC_PRIVATE, "private");
        MODIFIERS.put(CodeConstants.ACC_ABSTRACT, "abstract");
        MODIFIERS.put(CodeConstants.ACC_STATIC, "static");
        MODIFIERS.put(CodeConstants.ACC_FINAL, "final");
        MODIFIERS.put(CodeConstants.ACC_STRICT, "strictfp");
        MODIFIERS.put(CodeConstants.ACC_TRANSIENT, "transient");
        MODIFIERS.put(CodeConstants.ACC_VOLATILE, "volatile");
        MODIFIERS.put(CodeConstants.ACC_SYNCHRONIZED, "synchronized");
        MODIFIERS.put(CodeConstants.ACC_NATIVE, "native");
    }

    private static final int CLASS_ALLOWED =
            CodeConstants.ACC_PUBLIC | CodeConstants.ACC_PROTECTED | CodeConstants.ACC_PRIVATE | CodeConstants.ACC_ABSTRACT |
                    CodeConstants.ACC_STATIC | CodeConstants.ACC_FINAL | CodeConstants.ACC_STRICT;
    private static final int FIELD_ALLOWED =
            CodeConstants.ACC_PUBLIC | CodeConstants.ACC_PROTECTED | CodeConstants.ACC_PRIVATE | CodeConstants.ACC_STATIC |
                    CodeConstants.ACC_FINAL | CodeConstants.ACC_TRANSIENT | CodeConstants.ACC_VOLATILE;
    private static final int METHOD_ALLOWED =
            CodeConstants.ACC_PUBLIC | CodeConstants.ACC_PROTECTED | CodeConstants.ACC_PRIVATE | CodeConstants.ACC_ABSTRACT |
                    CodeConstants.ACC_STATIC | CodeConstants.ACC_FINAL | CodeConstants.ACC_SYNCHRONIZED | CodeConstants.ACC_NATIVE |
                    CodeConstants.ACC_STRICT;

    private static final int CLASS_EXCLUDED = CodeConstants.ACC_ABSTRACT | CodeConstants.ACC_STATIC;
    private static final int FIELD_EXCLUDED = CodeConstants.ACC_PUBLIC | CodeConstants.ACC_STATIC | CodeConstants.ACC_FINAL;
    private static final int METHOD_EXCLUDED = CodeConstants.ACC_PUBLIC | CodeConstants.ACC_ABSTRACT;

    private static final int ACCESSIBILITY_FLAGS = CodeConstants.ACC_PUBLIC | CodeConstants.ACC_PROTECTED | CodeConstants.ACC_PRIVATE;

    private static void appendModifiers(TextBuffer buffer, int flags, int allowed, boolean isInterface, int excluded) {
        flags &= allowed;
        if (!isInterface) excluded = 0;
        for (int modifier : MODIFIERS.keySet()) {
            if ((flags & modifier) == modifier && (modifier & excluded) == 0) {
                buffer.append(MODIFIERS.get(modifier)).append(' ');
            }
        }
    }

    public static GenericClassDescriptor getGenericClassDescriptor(StructClass cl) {
        if (DecompilerContext.getOption(IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES)) {
            StructGenericSignatureAttribute attr = cl.getAttribute(StructGeneralAttribute.ATTRIBUTE_SIGNATURE);
            if (attr != null) {
                return GenericMain.parseClassSignature(attr.getSignature());
            }
        }
        return null;
    }

    public static void appendTypeParameters(TextBuffer buffer, List<String> parameters, List<? extends List<GenericType>> bounds) {
        buffer.append('<');

        for (int i = 0; i < parameters.size(); i++) {
            if (i > 0) {
                buffer.append(", ");
            }

            buffer.append(parameters.get(i));

            List<GenericType> parameterBounds = bounds.get(i);
            if (parameterBounds.size() > 1 || !"java/lang/Object".equals(parameterBounds.get(0).value)) {
                buffer.append(" extends ");
                buffer.append(GenericMain.getGenericCastTypeName(parameterBounds.get(0)));
                for (int j = 1; j < parameterBounds.size(); j++) {
                    buffer.append(" & ");
                    buffer.append(GenericMain.getGenericCastTypeName(parameterBounds.get(j)));
                }
            }
        }

        buffer.append('>');
    }
}